/**
 * Copyright (C) 2010, 2011 Neofonie GmbH
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.dicodeproject.blogimport.parser;

import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Parses a whole xml document to a {@link BlogDocument} instance. Internally it
 * checks the xpaths of the content and meta nodes against blacklisting
 * criteria.
 * 
 * We need the woodstox parser for correct behavior, see:
 * 
 * <a href="https://jira.neofonie.de/browse/NES-157">Jira NES-157</a> and <a
 * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6976938">Sun
 * Bugtracker</a>.
 * 
 * 
 */

public class BlogParser {

  private static final Logger LOG = LoggerFactory.getLogger(BlogParser.class);

  private XMLInputFactory factory;

  /** Blacklisted nodes in content block of @ BlogDocument}. */
  private XPathBlacklist XPathBlacklist;

  /** Counter for parsing errors. */
  private static int errorCounter = 0;

  /**
   * Creates an instance of an {@link BlogParser} that will parse a all
   * non-blacklisted items of a blog xml into a {@link BlogDocument}.
   */
  public BlogParser() {
    try {
      this.factory = XMLInputFactory.newInstance("com.ctc.wstx.stax.WstxInputFactory", null);
    } catch (FactoryConfigurationError e) {
      LOG.error("Cannot create woodstox xml factory (com.ctc.wstx.stax.WstxInputFactory), using standard sun factory or first factory in classpath. This may lead to parsing problems.");
      this.factory = XMLInputFactory.newInstance();
    }

    // set coalesce adjacent character data enabled
    this.factory.setProperty(XMLInputFactory.IS_COALESCING, true);

    LOG.info("Using XMLInputFactory class: " + this.factory.getClass());
    if (this.factory.getClass().getProtectionDomain() != null
        && this.factory.getClass().getProtectionDomain().getCodeSource() != null) {
      LOG.info("Loaded from JAR: "
          + this.factory.getClass().getProtectionDomain().getCodeSource().getLocation());
    } else {
      URL location = this.factory.getClass().getResource(
          '/' + this.factory.getClass().getName().replace('.', '/') + ".class");
      LOG.info("Loaded from environment: " + location);
    }
    // blacklists
    this.XPathBlacklist = new XPathBlacklist();
  }

  /**
   * Parses a blog document.
   * 
   * @param content
   *          the xml file
   * @return a blog document
   * @throws ParsingException
   */
  public BlogDocument parse(final String content) throws ParsingException {
    if (content == null) {
      throw new ParsingException("Cannot parse 'null' content.");
    }
    try {
      InitDocument article = new InitDocument();
      XMLStreamReader reader = this.factory.createXMLStreamReader(new StringReader(content));
      int event = reader.getEventType();

      Map<String, String> contentMap = new HashMap<String, String>();
      List<String> xpathList = new ArrayList<String>();

      boolean inDates = false;

      StringBuilder characterContent = new StringBuilder();

      while (reader.hasNext()) {
        switch (event) {
        case XMLStreamConstants.START_ELEMENT: {

          String localName = reader.getLocalName();
          String url;

          if ("Document".equals(localName)) {
            url = reader.getAttributeValue("http://www.neofonie.de/xmlns/search/blast/", "uri");
            if (url.matches("^http://.*http://.*")) {
              String[] tmp = url.split(" ");
              for (String part : tmp) {
                try {
                  new URL(part);
                  url = part;
                  contentMap.put(ContentMapFields.URL.toString(), url);
                  if (LOG.isDebugEnabled()) {
                    LOG.debug("Multiple urls found, using part " + part);
                  }
                  break;
                } catch (MalformedURLException e) {
                  if (LOG.isDebugEnabled()) {
                    LOG.debug("Multiple urls found, part " + part + " is no valid url.");
                  }
                }
              }
            } else {
              contentMap.put(ContentMapFields.URL.toString(), url);
            }
          }

          if (localName.equals("Dates")) {
            inDates = true;
          }
          xpathList.add(localName);
          break;
        }
        case XMLStreamConstants.END_ELEMENT: {
          String tmp = characterContent.toString().trim();
          characterContent = new StringBuilder();
          if (tmp.length() > 0) {

            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < xpathList.size(); i++) {
              builder.append(xpathList.get(i));
              if (i != (xpathList.size() - 1)) {
                builder.append("_");
              }
            }
            // set possible index, if same xpathes exists
            this.findAndSetIndexes(contentMap, builder);
            String xpath = builder.toString();
            if (!this.XPathBlacklist.isBlacklisted(xpath)) {

              // transform timestamps from seconds to
              // milliseconds
              if (inDates && tmp.length() <= 10) {
                tmp = tmp + "000";
              }
              contentMap.put(xpath, tmp);
            }
          }
          String localName = reader.getLocalName();

          if (localName.equals("Dates")) {
            inDates = false;
          }
          xpathList.remove(localName);
          break;
        }
        case XMLStreamConstants.CHARACTERS: {
          String tmp = reader.getText();

          if (tmp != null) {
            characterContent.append(tmp);
          }
          break;
        }
        case XMLStreamConstants.START_DOCUMENT:
        case XMLStreamConstants.END_DOCUMENT:
        case XMLStreamConstants.ATTRIBUTE:
        case XMLStreamConstants.NAMESPACE:
          break;
        case XMLStreamConstants.CDATA: {
          LOG.warn("Unhandled CDATA found: " + reader.getText());
          break;
        }
        default:
          break;
        }
        event = reader.next();
      }

      if (LOG.isTraceEnabled()) {
        LOG.trace("contentMap, size=" + contentMap.size());
        for (Entry<String, String> entry : contentMap.entrySet()) {
          LOG.trace("contentMap=" + entry.getKey() + "  " + entry.getValue());
        }
      }

      article.setContentMap(contentMap);
      reader.close();
      return new BlogDocument(article);
    } catch (XMLStreamException e) {

      errorCounter++;

      // output the first 100 characters of the file
      String startString = "";

      if (content.length() > 0) {
        int sample = 100;
        if (content.length() < sample) {
          sample = content.length();
        }
        startString = content.substring(0, sample - 1);
      }
      throw new ParsingException("Document starts with: " + startString, e);
    }
  }

  /**
   * Find and set indexes.
   * 
   * @param map
   *          the map
   * @param xpathBuilder
   *          the xpath builder
   */
  private void findAndSetIndexes(Map<String, String> map, StringBuilder xpathBuilder) {

    // set possible index, if same xpathes exists
    int index = 0;
    for (String key : map.keySet()) {
      if (key.contains(xpathBuilder.toString())) {
        LOG.trace("incrementing index for " + key);
        index++;

      }
    }
    if (index > 0) {
      xpathBuilder.append("_" + index);
    }
  }
  
  /**
   * Counts the number of parsing errors.
   * Not used yet.
   * @return number of parsing errors
   */
  public static int getErrorCounter() {
    return errorCounter;
  }
}
